package de.mmis.core.base.gclass;

import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Stack;

import org.apache.log4j.Logger;

import de.mmis.core.base.Toolbox;

/**
 * class representing general Types in Java. Unlike java.lang.reflect.Type, it
 * supports no wildcards or typeparameters (they will be resolved if possible)
 * and can be instantiated from Types or Strings.
 * 
 * @author Christian Koch
 * 
 */
public abstract class GClass {

	private final static Logger LOGGER = Logger.getLogger(GClass.class);

	/**
	 * @param clazz
	 *            class that should be represented
	 * @return new GClass instance representing given Class object
	 */
	public static GClass fromClass(Class<?> clazz) {
		if (clazz.isArray())
			return new GArray(fromClass(clazz.getComponentType()));
		return new GParameterized(clazz, new GClass[0]);
	}

	/**
	 * @param typeString
	 *            string representing a type
	 * @param classLoader
	 *            class loader to use for loading classes, can be
	 *            <code>null</code>
	 * @return new GClass instance representing a type specified in the given
	 *         String
	 */
	public static GClass fromString(String typeString, ClassLoader classLoader)
			throws ClassNotFoundException {
		StringReader sr = new StringReader(typeString);
		return fromString(sr, classLoader);
	}

	/**
	 * @param typeStringStream
	 *            string stream representing a type
	 * @param classLoader
	 *            class loader to use for loading classes, can be
	 *            <code>null</code>
	 * @return new GClass instance representing a type specified in the given
	 *         string stream
	 */
	private static GClass fromString(StringReader typeStringStream,
			ClassLoader classLoader) throws ClassNotFoundException {
		try {
			StringBuilder sb = new StringBuilder();
			typeStringStream.mark(0);
			int cur = typeStringStream.read();
			while (cur != -1 && cur != '<' && cur != '>' && cur != '['
					&& cur != ',') {
				sb.append((char) cur);
				typeStringStream.mark(0);
				cur = typeStringStream.read();
			}
			if (cur != -1)
				typeStringStream.reset();

			Class<?> c = null;
			try {
				c = Toolbox.forName(sb.toString(), classLoader);
			} catch (ClassNotFoundException e) {
				LOGGER.warn(
						"GClass.fromString: cannot load class with given class loader, try default loader",
						e);
			}
			if (c == null)
				c = Class.forName(sb.toString());

			GClass base = new GParameterized(c, new GClass[0]);

			if (cur == '<') {
				cur = typeStringStream.read();
				ArrayList<GClass> tmp = new ArrayList<GClass>();
				while (cur != -1 && cur != '>') {
					tmp.add(fromString(typeStringStream, classLoader));
					cur = typeStringStream.read();
				}
				((GParameterized) base).setTypeParameterClass(tmp
						.toArray(new GClass[0]));
			}

			while (cur == '[') {
				if (typeStringStream.read() != ']')
					throw new ClassNotFoundException("invalid array brackets");
				base = new GArray(base);
				cur = typeStringStream.read();
			}
			return base;
		} catch (IOException e) {
			throw new ClassNotFoundException("StringReader failed", e);
		}
	}

	/**
	 * write String representation of itself into given StringBuilder
	 * 
	 * @param sb
	 *            StringBuilder to write to
	 */
	protected abstract void toStringInternal(StringBuilder sb);

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		toStringInternal(sb);
		return sb.toString();
	}

	@Override
	public abstract boolean equals(Object obj);

	@Override
	public abstract int hashCode();

	/**
	 * @param clazz
	 *            class to compare with
	 * @return true, if this GClass instance representing the given class, els
	 *         false
	 */
	public abstract boolean equalsClass(Class<?> clazz);

	/**
	 * @return base class of this type (omitting type parameters)
	 */
	public abstract Class<?> getBaseClass();

	/**
	 * determines the actual used type parameters for specified baseclass
	 * 
	 * @param curtype
	 *            type to find type parameters for
	 * @param baseclass
	 *            class whose type parameters should be determined
	 * @return an array of the actual used type parameters, in order of
	 *         declaration
	 */
	public static GClass[] getGenericsOf(GClass curtype, Class<?> baseclass) {
		Hashtable<TypeVariable<?>, GClass> tmap = new Hashtable<TypeVariable<?>, GClass>();
		Class<?> curclass = curtype.getBaseClass();
		if (curclass == null)
			return null;

		GClass[] actualParameterClasses = ((GParameterized) curtype)
				.getTypeParameterClass();
		for (int i = 0; i < curclass.getTypeParameters().length; i++)
			tmap.put(
					curclass.getTypeParameters()[i],
					(i < actualParameterClasses.length ? actualParameterClasses[i]
							: GClass.fromClass(Object.class)));
		return getGenericsOf(curclass, baseclass, tmap);
	}

	public static void buildTypeTree(Type curtype,
			Hashtable<TypeVariable<?>, GClass> typeVariableMap) {
		Class<?> curclass;

		if (curtype instanceof ParameterizedType) {
			curclass = (Class<?>) ((ParameterizedType) curtype).getRawType();

			TypeVariable<?>[] typevars = curclass.getTypeParameters();
			Type[] types = ((ParameterizedType) curtype)
					.getActualTypeArguments();
			for (int i = 0; i < typevars.length; i++)
				typeVariableMap.put(typevars[i],
						fromType(types[i], typeVariableMap));
			// ret[i]=GClass.fromType(types[i], tmap);
		} else if (curtype instanceof Class<?>)
			curclass = ((Class<?>) curtype);
		else
			return;

		Type superclasstype = curclass.getGenericSuperclass();
		if (superclasstype != null)
			buildTypeTree(superclasstype, typeVariableMap);

		for (Type interfacetype : curclass.getGenericInterfaces())
			buildTypeTree(interfacetype, typeVariableMap);
		return;
	}

	/**
	 * determines the actual used type parameters for specified baseclass
	 * 
	 * @param curtype
	 *            type to find type parameters for
	 * @param baseclass
	 *            class whose type parameters should be determined
	 * @param typevarmap
	 *            mapping of type variables to actually used types
	 * @return an array of the actual used type parameters, in order of
	 *         declaration
	 */
	public static GClass[] getGenericsOf(Type curtype, Class<?> baseclass,
			Hashtable<TypeVariable<?>, GClass> typevarmap) {
		if (typevarmap == null)
			typevarmap = new Hashtable<TypeVariable<?>, GClass>();

		GClass[] ret;

		Class<?> curclass;

		if (curtype instanceof ParameterizedType) {
			curclass = (Class<?>) ((ParameterizedType) curtype).getRawType();

			TypeVariable<?>[] typevars = curclass.getTypeParameters();
			Type[] types = ((ParameterizedType) curtype)
					.getActualTypeArguments();
			for (int i = 0; i < typevars.length; i++)
				typevarmap.put(typevars[i], fromType(types[i], typevarmap));
			// ret[i]=GClass.fromType(types[i], tmap);
		} else if (curtype instanceof Class<?>)
			curclass = ((Class<?>) curtype);
		else
			return null;

		if (curclass == baseclass) {
			TypeVariable<?>[] typevars = curclass.getTypeParameters();
			ret = new GClass[typevars.length];
			for (int i = 0; i < typevars.length; i++) {
				ret[i] = typevarmap.get(typevars[i]);
				if (ret[i] == null)
					ret[i] = GClass.fromType(typevars[i].getBounds()[0],
							typevarmap);
			}
			return ret;
		}

		Type superclasstype = curclass.getGenericSuperclass();
		if (superclasstype != null) {
			ret = getGenericsOf(superclasstype, baseclass, typevarmap);
			if (ret != null)
				return ret;
		}

		for (Type interfacetype : curclass.getGenericInterfaces()) {
			ret = getGenericsOf(interfacetype, baseclass, typevarmap);
			if (ret != null)
				return ret;
		}
		return null;
	}

	/**
	 * generate new GClass instance from given type
	 * 
	 * @param type
	 *            type to convert
	 * @param typevarmap
	 *            mapping of type variables to actually used types (in/out)
	 * @return new GClass instance representing specified Type
	 */
	public static GClass fromType(Type type,
			Hashtable<TypeVariable<?>, GClass> typevarmap) {
		return fromTypeInternal(type, typevarmap, new Stack<TypeVariable<?>>());
	}

	protected abstract void collectClasses(List<Class<?>> classList);

	public ClassLoader getCommonClassLoader() {
		List<Class<?>> classList = new ArrayList<Class<?>>();
		collectClasses(classList);
		ClassLoader ret = Toolbox.findCommonClassLoader(classList
				.toArray(new Class<?>[classList.size()]));
		if (ret != null)
			return ret;
		Class<?> baseClass = getBaseClass();
		if (baseClass != null && baseClass.getClassLoader() != null)
			return baseClass.getClassLoader();
		return GClass.class.getClassLoader();
	}

	private static GClass fromTypeInternal(Type type,
			Hashtable<TypeVariable<?>, GClass> typevarmap,
			Stack<TypeVariable<?>> usedTypeVarStack) {
		if (typevarmap == null)
			typevarmap = new Hashtable<TypeVariable<?>, GClass>();
		if (type instanceof TypeVariable<?> && typevarmap.containsKey(type)) {
			return typevarmap.get(type);
		}
		if (type instanceof Class<?>) {
			return GClass.fromClass((Class<?>) type);
		} else if (type instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) type;
			Type rawtype = pt.getRawType();
			if (!(rawtype instanceof Class<?>))
				throw new IllegalArgumentException(
						"given type no valid parameterized type: raw type "
								+ rawtype + " is no class");
			Class<?> clazz = (Class<?>) rawtype;
			GClass[] typeParameters = new GClass[pt.getActualTypeArguments().length];
			for (int i = 0; i < pt.getActualTypeArguments().length; i++) {
				typeParameters[i] = GClass.fromTypeInternal(
						pt.getActualTypeArguments()[i], typevarmap,
						usedTypeVarStack);
				typevarmap.put(clazz.getTypeParameters()[i], typeParameters[i]);
			}
			return new GParameterized(clazz, typeParameters);
		} else if (type instanceof TypeVariable<?>) {
			Type[] upper = ((TypeVariable<?>) type).getBounds();
			if (upper.length == 1) {
				if (usedTypeVarStack.contains(type))// recursive descend
													// detected, simply return
													// raw type
				{
					Type rettype = upper[0];
					while (rettype instanceof WildcardType
							|| rettype instanceof TypeVariable<?>
							|| rettype instanceof GenericArrayType) {
						while (rettype instanceof WildcardType)
							rettype = ((WildcardType) rettype).getUpperBounds()[0];
						while (rettype instanceof TypeVariable<?>)
							rettype = ((TypeVariable<?>) rettype).getBounds()[0];
						while (rettype instanceof GenericArrayType)
							rettype = ((GenericArrayType) rettype)
									.getGenericComponentType();
					}
					if (rettype instanceof ParameterizedType)
						return GClass
								.fromClass((Class<?>) ((ParameterizedType) rettype)
										.getRawType());
					else if (rettype instanceof Class<?>)
						return GClass.fromClass((Class<?>) rettype);
					else
						return GClass.fromClass(Object.class);
				}

				usedTypeVarStack.push((TypeVariable<?>) type);
				GClass ret = GClass.fromTypeInternal(upper[0], typevarmap,
						usedTypeVarStack);
				usedTypeVarStack.pop();
				return ret;
			}
			return GClass.fromTypeInternal(Object.class, typevarmap,
					usedTypeVarStack);
		} else if (type instanceof WildcardType) {
			Type[] upper = ((WildcardType) type).getUpperBounds();
			if (upper.length == 1)
				return GClass.fromTypeInternal(upper[0], typevarmap,
						usedTypeVarStack);
			return GClass.fromTypeInternal(Object.class, typevarmap,
					usedTypeVarStack);
		} else if (type instanceof GenericArrayType) {
			return new GArray(GClass.fromTypeInternal(
					((GenericArrayType) type).getGenericComponentType(),
					typevarmap, usedTypeVarStack));
		} else
			throw new IllegalArgumentException(
					"given type is none of the five subtypes: " + type
							+ " is of class " + type.getClass());
	}
}
